#
# Copyright 2005 Christian Neukirchen
# http://chneukirchen.org 
# 

module Dynamic
  class << self
    Thread.main[:DYNAMIC] = Hash.new { |hash, key|
      raise NameError, "no such dynamic variable: #{key}"
    }
    
    def here!
      Thread.current[:DYNAMIC] = Hash.new { |hash, key|
        raise NameError, "no such dynamic variable: #{key}"
      }.update Thread.main[:DYNAMIC]
    end
    
    def variables
      Thread.current[:DYNAMIC] or here!
    end
    
    def variable(definition)
      case definition
      when Symbol
        if variables.has_key? definition
          raise NameError, "dynamic variable `#{definition}' already exists"
        end
        variables[definition] = nil
      when Hash
        definition.each { |key, value|
          if variables.has_key? key
            raise NameError, "dynamic variable `#{key}' already exists"
          end
          variables[key] = value
        }
      else
        raise ArgumentError,
        "can't create a new dynamic variable from #{definition.class}"
      end
    end
    
    def [](key)
      variables[key]
    end
    
    def []=(key, value)
      variables[key] = value
    end
    
    def undefine(*keys)
      keys.each { |key|
        self[key]
        variables.delete key
      }
    end
    
    def let(bindings, &block)
      save = {}
      bindings.to_hash.collect { |key, value|
        save[key] = self[key]
        self[key] = value
      }
      block.call
      variables.update save
    end
    
    def method_missing(name, *args)
      if match = /=\Z/.match(name.to_s)    # setter?
        raise ArgumentError, "invalid setter call"  unless args.size == 1
        self[match.pre_match.intern] = args.first
      else
        raise ArgumentError, "invalid getter call"  unless args.empty?
        self[name]
      end
    end
  end
end

if $0 == __FILE__
  require 'test/unit'
  
  class DynamicTest < Test::Unit::TestCase
    def test_01_variable
      Dynamic.variable :foo
      assert_nil Dynamic[:foo]
      
      Dynamic.variable :bar => 5
      assert_equal 5, Dynamic[:bar]
    end
    
    def test_02_raise
      assert_raise(NameError) {
        Dynamic[:notyethere]
      }
      Dynamic.variable :notyethere
      assert_nil Dynamic[:notyethere]
      
      assert_raise(NameError) {
        Dynamic.variable :notyethere
      }
      assert_raise(NameError) {
        Dynamic.variable :notyethere => 9
      }
      assert_raise(NameError) {
        Dynamic.undefine :blub
      }
    end
    
    def test_03_set
      Dynamic.variable :setme
      assert_nil Dynamic[:setme]
      Dynamic[:setme] = 23
      assert_equal 23, Dynamic[:setme]
      Dynamic[:setme] = 32
      assert_equal 32, Dynamic[:setme]
    end
    
    def test_04_convenience
      Dynamic.variable :bla => 4
      assert_equal 4, Dynamic[:bla]
      assert_equal 4, Dynamic.bla
      Dynamic.bla = 5
      assert_equal 5, Dynamic.bla
      Dynamic.bla = nil
      assert_nil Dynamic.bla
      Dynamic.undefine :bla
      assert_raise(NameError) {
        Dynamic.bla
      }
    end
    
    def test_05_let
      Dynamic.variable :one   => :one
      Dynamic.variable :two   => :two
      Dynamic.variable :three => :three
      
      assert_equal [:one, :two, :three],[Dynamic.one, Dynamic.two, Dynamic.three]
      
      Dynamic.let(:one => 1, :two => :zwei) {
        # Check new binding
        assert_equal [1, :zwei, :three],[Dynamic.one, Dynamic.two, Dynamic.three]
        
        # Check update
        Dynamic.two = 2
        assert_equal [1, 2, :three],[Dynamic.one, Dynamic.two, Dynamic.three]
        
        # Check propagation
        Dynamic.three = 3
        assert_equal [1, 2, 3],[Dynamic.one, Dynamic.two, Dynamic.three]
      }
      
      # Only three has propgated
      assert_equal [:one, :two, 3],[Dynamic.one, Dynamic.two, Dynamic.three]
    end
    
    def test_06_thread
      Dynamic.variable :threaded_a => :a
      Dynamic.variable :threaded_b => :b
      
      th = Thread.new {
        assert_equal :a, Dynamic.threaded_a
        assert_equal :b, Dynamic.threaded_b
        Dynamic.threaded_a = 1    # Will not propagate, due to thread locality
      }
      th.join
      
      assert_equal :a, Dynamic.threaded_a
      assert_equal :b, Dynamic.threaded_b
    end
  end
end
